/*******************************************************************************************
 * Copyright (c) 2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Record;
using Nvigorate.Relational.State;
using Nvigorate.Xml;

namespace Nvigorate.Relational.Mapping
{
    [Serializable]
    public abstract class BindingMapBase : IBindingMap, IXmlSerializable, ILinqToXml
    {
        #region Protected Fields

        protected bool _canDelete = true;
        protected bool _canInsert = true;
        protected bool _canSelect = true;
        protected bool _canUpdate = true;
        protected string _repositoryName = "";
        protected string _targetName = "";
        protected Type _targetType;
        protected List<ITargetRelationship> _relationships = new List<ITargetRelationship>();
        protected Dictionary<string, ISourceMap> _sources = new Dictionary<string, ISourceMap>();
        private IList<Type> _inherits = new List<Type>();
        private IList<Type> _inheritedBy = new List<Type>();
        protected IEnumerable<IJoin> joins
        {
            get
            {
                return _sources.Values.Select(s => s.Join).Where(j => j != null);
            }
        }

        #endregion

        #region Implementation of ILinqToXml

        private string GetForbiddenValue()
        {
            DelimitedBuilder forbidden = new DelimitedBuilder(",");
            if (!CanInsert)
                forbidden.Append("insert");
            if (!CanUpdate)
                forbidden.Append("update");
            if (!CanDelete)
                forbidden.Append("delete");
            if (!CanSelect)
                forbidden.Append("select");

            return forbidden.ToString();
        }

        private void ParseForbiddenValue(string forbidden)
        {
            CanInsert = !forbidden.Contains("insert");
            CanUpdate = !forbidden.Contains("update");
            CanDelete = !forbidden.Contains("delete");
            CanSelect = !forbidden.Contains("select");
        }

        public XElement GetXElementRepresentation()
        {
            var element = new XElement("map",
                                       new XAttribute("repositoryName", _repositoryName),
                                       new XAttribute("target", _targetName),
                                       new XAttribute("forbidden", GetForbiddenValue()),
                                       new XElement
                                           ("sources",
                                            _sources.Select(kvp => kvp.Value).Select(m => m.GetXElementRepresentation())
                                           ),
                                       new XElement
                                           ("relatedTo",
                                            _relationships.Select(r => r.GetXElementRepresentation())
                                           )
                );
            return element;
        }

        public object ParseXElement(XElement element)
        {
            _repositoryName = element.GetAttributeValue("repositoryName") ?? "";
            _targetName = element.GetAttributeValue("target") ?? "";
            ParseForbiddenValue(element.GetAttributeValue("forbidden") ?? "");
            _sources = element
                .Descendants("source")
                .Select(e => GetSourceMap(e))
                .ToDictionary(s => s.SourceName);
            _relationships = element
                .Descendants("relationship")
                .Select(e =>
                    {
                        if(EnumHelper.Parse<Relationship>(e.GetAttributeValue("type"), true).Equals(Relationship.Peer))
                        {
                            return new ManyToManyRelationship().ParseXElement(e) as ITargetRelationship;
                        }
                        else
                        {
                            return new OneToManyRelationship().ParseXElement(e) as ITargetRelationship;
                        }
                    })
                .ToList();

            _relationships.ForEach(r => r.SubjectType = TargetType);
            return this;
        }

        #endregion

        #region Implementation of IXmlSerializable

        /// <summary>
        /// This method is reserved and should not be used. When implementing the IXmlSerializable interface, you should return null (Nothing in Visual Basic) from this method, and instead, if specifying a custom schema is required, apply the <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute" /> to the class.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Xml.Schema.XmlSchema" /> that describes the XML representation of the object that is produced by the <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)" /> method and consumed by the <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)" /> method.
        /// </returns>
        public XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// Generates an object from its XML representation.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader" /> stream from which the object is deserialized. </param>
        public void ReadXml(XmlReader reader)
        {
            ParseXElement(XElement.Load(reader));
        }

        /// <summary>
        /// Converts an object into its XML representation.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlWriter" /> stream to which the object is serialized. </param>
        public void WriteXml(XmlWriter writer)
        {
            GetXElementRepresentation().WriteTo(writer);
        }

        #endregion

        #region Implementation of IBindingMap

        /// <summary>
        /// Gets or sets a value indicating whether the delete operation is available 
        /// for the mapped type.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance can delete; otherwise, <c>false</c>.
        /// </value>
        public bool CanDelete
        {
            get { return _canDelete; }
            set { _canDelete = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether insert operation is available 
        /// for the mapped type.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance can insert; otherwise, <c>false</c>.
        /// </value>
        public bool CanInsert
        {
            get { return _canInsert; }
            set { _canInsert = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the select operation is available 
        /// for the mapped type.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance can select; otherwise, <c>false</c>.
        /// </value>
        public bool CanSelect
        {
            get { return _canSelect; }
            set { _canSelect = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the update operation is available 
        /// for the mapped type.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance can update; otherwise, <c>false</c>.
        /// </value>
        public bool CanUpdate
        {
            get { return _canUpdate; }
            set { _canUpdate = value; }
        }

        /// <summary>
        /// In inheritence scenarios where the type returned from the database
        /// is determinate upon the results retrieved, the discriminator
        /// provides the decision criteria for determining which type
        /// should be created on a per record basis
        /// </summary>
        public IDiscriminate Discriminator { get; set; }

        /// <summary>
        /// Gets the child types which inherit their map from this one
        /// </summary>
        /// <returns>The child types which inherit from this</returns>
        public IEnumerable<Type> GetChildTypes()
        {
            return Reflector.GetInheritenceChain(TargetType);
        }

        /// <summary>
        /// The types which this map inherits the map from
        /// </summary>
        public IList<Type> Inherits
        {
            get { return _inherits; }
            set { _inherits = value; }
        }

        /// <summary>
        /// Provides a list of types which inherit from this map
        /// </summary>
        public IList<Type> InheritedBy
        {
            get { return _inheritedBy; }
            set { _inheritedBy = value; }
        }

        /// <summary>
        /// The name of the source which this map relates to
        /// </summary>
        public string RepositoryName
        {
            get { return _repositoryName; }
            set { _repositoryName = value; }
        }

        /// <summary>
        /// The name of the target entity
        /// </summary>
        public string TargetName
        {
            get { return _targetName; }
            set { _targetName = value; }
        }

        /// <summary>
        /// The type of the target entity
        /// </summary>
        public Type TargetType
        {
            get
            {
                if (_targetType == null)
                    _targetType = Type.GetType(TargetName);
                return _targetType;
            }
            set { _targetType = value; }
        }

        #region Implementation of IBindingMap

        /// <summary>
        /// A list of relationships between this mapped type and others
        /// </summary>
        public List<ITargetRelationship> Relationships
        {
            get { return _relationships; }
            set { _relationships = value; }
        }

        #endregion

        /// <summary>
        /// The sources that combine to populate the target
        /// </summary>
        public Dictionary<string, ISourceMap> Sources
        {
            get { return _sources; }
            set { _sources = value; }
        }

        /// <summary>
        /// Adds an assignment to an existing SourceMap
        /// </summary>
        /// <param name="assignment">The assignment to add</param>
        public virtual void AddAssignment(IAssignment assignment)
        {
            string key = assignment.Source.Entity;
            if (!_sources.ContainsKey(key))
                _sources.Add(key, new SourceMap(assignment.Target.Entity) {SourceName = key});

            _sources[key].Assignments.Add(assignment);
        }

        public virtual void AddAssignments(IEnumerable<IAssignment> assignments)
        {
            assignments
                .ToList()
                .ForEach(AddAssignment);
        }

        /// <summary>
        /// Allow adding a source map to the binding definition
        /// </summary>
        /// <param name="map"></param>
        public virtual void AddSource(ISourceMap map)
        {
            if (!_sources.ContainsKey(map.SourceName))
                _sources.Add(map.SourceName, map);
        }

        /// <summary>
        /// Retrieve all assignments in the map
        /// </summary>
        /// <returns>A list of all assignments</returns>
        public virtual IEnumerable<IAssignment> GetAssignments()
        {
            return _sources.SelectMany(m => m.Value.Assignments);
        }

        /// <summary>
        /// Retrieve a list of assignments across all sources which match the predicate
        /// </summary>
        /// <param name="predicate">The predicate to search by</param>
        /// <returns>A list of assignments across all sources which match the predicate</returns>
        public virtual IEnumerable<IAssignment> GetAssignmentsByPredicate(Func<IAssignment, bool> predicate)
        {
            return _sources.SelectMany(m => m.Value.Assignments).Where(predicate);
        }

        /// <summary>
        /// Get the assignment for the source field
        /// </summary>
        /// <param name="source"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        public virtual IAssignment GetAssignmentBySourceField(string source, string field)
        {
            return GetAssignmentsByPredicate(a => a.Source.Entity == source && a.Source.Field == field).FirstOrDefault();
        }

        /// <summary>
        /// Get the assignment for the target field (given the source name)
        /// </summary>
        /// <param name="source"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        public virtual IAssignment GetAssignmentByTargetField(string source, string field)
        {
            return GetAssignmentsByPredicate(a => a.Source.Entity == source && a.Target.Field == field).FirstOrDefault();
        }

        /// <summary>
        /// Get the assignments for the target field
        /// </summary>
        /// <param name="field"></param>
        /// <returns></returns>
        public virtual IEnumerable<IAssignment> GetAssignmentsByTargetField(string field)
        {
            return GetAssignmentsByPredicate(a => a.Target.Field == field);
        }

        /// <summary>
        /// Returns child source maps
        /// </summary>
        /// <returns>Returns all maps for the type except the root</returns>
        public IEnumerable<ISourceMap> GetChildSources()
        {
            return _sources.Values.Where(s => s.Join != null);
        }

        /// <summary>
        /// Returns child source maps
        /// </summary>
        /// <returns>Returns all child maps of the parent map for the type</returns>
        public IEnumerable<ISourceMap> GetChildSources(string parentSourceName)
        {
            return _sources.Values.Where(s => s.Join != null && s.Join.Subject == parentSourceName);
        }

        /// <summary>
        /// Find a relationship to the related type for this mapped type
        /// </summary>
        /// <param name="relatedType">The subject or relative type</param>
        /// <returns>The relationship which ties this mapped type to the relatedType</returns>
        public T GetRelationshipForType<T>(Type relatedType) where T : ITargetRelationship
        {
            return (T) _relationships
                           .Where(r => r.GetType().Equals(typeof (T)))
                           .FirstOrDefault(r => r.RelativeType == relatedType || r.SubjectType == relatedType);
        }

        /// <summary>
        /// Retrieves the name of the root source
        /// </summary>
        /// <returns></returns>
        public virtual string GetRootName()
        {
            if (joins.Count() == 0)
                return _sources.Values.First().SourceName;

            IJoin rootJoin = GetRootJoin();

            if (rootJoin != null)
                return rootJoin.Subject;

            throw new Exception("Ain't no sech critter");
        }

        /// <summary>
        /// Retrieves the root source map
        /// </summary>
        /// <returns></returns>
        public virtual ISourceMap GetRoot()
        {
            if (joins.Count() == 0)
                return _sources.Values.First();

            IJoin rootJoin = GetRootJoin();

            if (rootJoin != null)
                return _sources[rootJoin.Subject];

            throw new Exception("Uuuuuugggggh. Why do you suck?");
        }

        /// <summary>
        /// Gets the source maps in create order
        /// </summary>
        /// <returns>The source maps in create order</returns>
        public IEnumerable<ISourceMap> GetSourceMapsInCreateOrder(bool reverse)
        {
            if(Sources.Values.Any(s => s.CreateOrder > 0))
            {
                var maps = Sources.Values.OrderBy(s => s.CreateOrder);
                return reverse ? maps.Reverse() : maps;
            }
            else
            {
                var maps = CrawlDependencies(GetRoot());
                return reverse ? maps.Reverse() : maps;
            }
        }

        private IEnumerable<ISourceMap> CrawlDependencies(ISourceMap map)
        {
            yield return map;
            var sourceTree = Sources.Values.Where(s => s.Join != null && s.Join.Subject == map.SourceName)
                .Select(s =>
                        CrawlDependencies(s)
                ).Flatten();

            foreach (var sourceMap in sourceTree)
            {
                yield return sourceMap;
            }
        }

        /// <summary>
        /// Returns a list of assignments provided by the source record
        /// </summary>
        /// <param name="sourceRecord">The record containing values for assignment points</param>
        /// <returns>A list of assignments provided by the source record</returns>
        public virtual IEnumerable<IAssignment> GetSuppliedAssignments(IRecord sourceRecord)
        {
            return GetAssignments().Where(a => sourceRecord.HasFieldForAssignment(a.Target));
        }

        /// <summary>
        /// Marks which sources were used to create the previous persist call
        /// </summary>
        /// <param name="state">The instance</param>
        public virtual void MarkPersistedSources(IPersistenceState state)
        {
            Sources
                .Values
                .ToList()
                .ForEach(s =>
                    {
                        if (!state.Loaded.Contains(s.SourceName))
                            state.Loaded.Add(s.SourceName);
                    });
        }

        /// <summary>
        /// Marks which properties on the instance are considered party of the identifying key
        /// </summary>
        /// <param name="state">The instance to mark</param>
        public virtual void MarkTargetKeys(IPersistenceState state)
        {
            state.Keys = GetAssignments()
                .Where(a => a.PrimaryKey)
                .Select(a => a.Target.Field)
                .ToList();
        }

        /// <summary>
        /// Merges a parent map into this one
        /// </summary>
        /// <param name="parentMap"></param>
        public void Merge(IBindingMap parentMap)
        {
            if(Inherits.Contains(parentMap.TargetType))
                return;

            parentMap.Sources.ForEach(s =>
                {
                    if(Sources.ContainsKey(s.Key))
                    {
                        MergeSourceMap(Sources[s.Key], s.Value);
                    }
                    else
                    {
                        Sources[s.Key] = TranslateSourceMap(s.Value);    
                    }
                });

            parentMap.Relationships.ForEach(r =>
                {
                    if (
                        !Relationships.Exists(
                             existing => existing.RelativeProperty == r.RelativeProperty))
                    {
                        Relationships.Add(TranslateRelationship(r));
                    }
                });

            parentMap.InheritedBy.Add(TargetType);
            Inherits.Add(parentMap.TargetType);
        }

        private ISourceMap TranslateSourceMap(ISourceMap map)
        {
            var newMap = Reflector.Clone(map);
            newMap.Assignments.ForEach(a =>
                {
                    a.Target.Entity = TargetType.GetCleanAssemblyQualifiedTypeName(); 
                });
            return newMap;
        }

        private void MergeSourceMap(ISourceMap sourceMap, ISourceMap map)
        {
            var newMap = Reflector.Clone(map);
            newMap.Assignments.ForEach(a =>
            {
                if(!sourceMap.Assignments.Any(o => o.Target.Field == a.Target.Field))
                    a.Target.Entity = TargetType.GetCleanAssemblyQualifiedTypeName();
            });
        }

        private ITargetRelationship TranslateRelationship(ITargetRelationship relationship)
        {
            var newRelationship = Reflector.Clone(relationship);

            newRelationship.Subject = TargetType.GetCleanAssemblyQualifiedTypeName();
            newRelationship.SubjectType = TargetType;
            return newRelationship;
        }

        #endregion

        #region Protected Methods

        //Determines the root source's join
        protected virtual IJoin GetRootJoin()
        {
            var joinList = joins.ToList();
            return joinList
                .Where(j => !joinList.Exists(cj => cj.Relative == j.Subject))
                .FirstOrDefault();
        }

        protected virtual ISourceMap GetSourceMap(XElement element)
        {
            var s = new SourceMap(_targetName);
            s.ParseXElement(element);
            return s;
        }

        #endregion

    }
}